// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef __TEST_DOCKER_ARCHIVE_HPP__
#define __TEST_DOCKER_ARCHIVE_HPP__

#include <process/collect.hpp>
#include <process/future.hpp>
#include <process/owned.hpp>

#include <stout/error.hpp>
#include <stout/json.hpp>
#include <stout/nothing.hpp>
#include <stout/os.hpp>
#include <stout/path.hpp>
#include <stout/stringify.hpp>
#include <stout/try.hpp>

#include "common/command_utils.hpp"

#include "tests/containerizer/rootfs.hpp"

using process::Failure;
using process::Future;

namespace mesos {
namespace internal {
namespace tests {

// This represents a docker archive. It has the same format
// as that tarball generated by doing a 'docker save'.
class DockerArchive
{
public:
  // Create a docker test image tarball in docker registry directory.
  // Users can define own entrypoint/cmd as JSON array of JSON string
  // (e.g., `[\"sh\", \"-c\"]`).
  static Future<Nothing> create(
      const std::string& directory,
      const std::string& name,
      const std::string& entrypoint = "null",
      const std::string& cmd = "null")
  {
    Try<Nothing> mkdir = os::mkdir(directory, true);
    if (mkdir.isError()) {
      return Failure("Failed to create '" + directory + "': " + mkdir.error());
    }

    const std::string imagePath = path::join(directory, name);

    mkdir = os::mkdir(imagePath);
    if (mkdir.isError()) {
      return Failure("Failed to create docker test image directory '" +
                     imagePath + "': " + mkdir.error());
    }

    const std::string layerId =
      "815b809d588c80fd6ddf4d6ac244ad1c01ae4cbe0f91cc7480e306671ee9c346";

    const std::string layerPath = path::join(imagePath, layerId);

    // Create docker test image `repositories`.
    JSON::Value repositories = JSON::parse(strings::format(
        R"~(
        {
            "%s": {
                "latest": "%s"
            }
        })~",
        name,
        layerId).get()).get();

    Try<Nothing> write = os::write(
        path::join(imagePath, "repositories"),
        stringify(repositories));

    if (write.isError()) {
      return Failure("Failed to save docker test image 'repositories': " +
                     write.error());
    }

    mkdir = os::mkdir(layerPath);
    if (mkdir.isError()) {
      return Failure("Failed to create docker test image layer '" +
                     layerId + "': " + mkdir.error());
    }

    JSON::Value manifest = JSON::parse(strings::format(
        R"~(
        {
            "id": "815b809d588c80fd6ddf4d6ac244ad1c01ae4cbe0f91cc7480e306671ee9c346",
            "created": "2016-03-02T17:16:00.167415955Z",
            "container": "eb53609036555d26c39bdccfa9850426934bdfde96111d099041689b2251a377",
            "container_config": {
                "Hostname": "eb5360903655",
                "Domainname": "",
                "User": "",
                "AttachStdin": false,
                "AttachStdout": false,
                "AttachStderr": false,
                "Tty": false,
                "OpenStdin": false,
                "StdinOnce": false,
                "Env": null,
                "Cmd": [
                    "/bin/sh",
                    "-c",
                    "#(nop) ADD file:81ba6f20bdb99e6c13c434a577069860b6656908031162083b1ac9c02c71dd9f in /"
                ],
                "Image": "",
                "Volumes": null,
                "WorkingDir": "",
                "Entrypoint": null,
                "OnBuild": null,
                "Labels": null
            },
            "docker_version": "1.9.1",
            "config": {
                "Hostname": "eb5360903655",
                "Domainname": "",
                "User": "",
                "AttachStdin": false,
                "AttachStdout": false,
                "AttachStderr": false,
                "Tty": false,
                "OpenStdin": false,
                "StdinOnce": false,
                "Env": null,
                "Cmd": %s,
                "Image": "",
                "Volumes": null,
                "WorkingDir": "",
                "Entrypoint": %s,
                "OnBuild": null,
                "Labels": null
            },
            "architecture": "amd64",
            "os": "linux"
        })~",
        entrypoint,
        cmd).get()).get();

    write = os::write(
        path::join(layerPath, "json"),
        stringify(manifest));

    if (write.isError()) {
      return Failure("Failed to save docker test image layer '" + layerId +
                     "': " + write.error());
    }

    const std::string rootfsDir = path::join(layerPath, "layer");

    mkdir = os::mkdir(rootfsDir);
    if (mkdir.isError()) {
      return Failure("Failed to create layer rootfs directory '" +
                     rootfsDir + "': " + mkdir.error());
    }

    // Create one linux rootfs for the layer.
    Try<process::Owned<Rootfs>> rootfs = LinuxRootfs::create(rootfsDir);
    if (rootfs.isError()) {
      return Failure("Failed to create docker test image rootfs: " +
                     rootfs.error());
    }

    Future<Nothing> tarRootfs = command::tar(
        Path("."),
        Path(path::join(layerPath, "layer.tar")),
        rootfsDir);

    tarRootfs.await();

    if (!tarRootfs.isReady()) {
      return Failure(
          "Failed to tar root filesystem: " +
          (tarRootfs.isFailed() ? tarRootfs.failure() : "discarded"));
    }

    Try<Nothing> rmdir = os::rmdir(rootfsDir);
    if (rmdir.isError()) {
        return Failure("Failed to remove layer rootfs directory: " +
                       rmdir.error());
    }

    write = os::write(
        path::join(layerPath, "VERSION"),
        "1.0");

    if (write.isError()) {
      return Failure("Failed to save layer version: " + write.error());
    }

    Future<Nothing> tarImage = command::tar(
        Path("."),
        Path(path::join(directory, name + ".tar")),
        imagePath);

    tarImage.await();

    if (!tarImage.isReady()) {
      return Failure(
          "Failed to tar docker test image: " +
          (tarImage.isFailed() ? tarImage.failure() : "discarded"));
    }

    rmdir = os::rmdir(imagePath);
    if (rmdir.isError()) {
      return Failure("Failed to remove image directory: " +
                     rmdir.error());
    }

    return Nothing();
  }
};

} // namespace tests {
} // namespace internal {
} // namespace mesos {

#endif // __TEST_DOCKER_ARCHIVE_HPP__
